库和链接简介

库、链接、初始化和 C++ 系列的第 1 部分

作者:Darryl Gove 和 Stephen Clamage,2011 年 5 月

第 1 部分 — 库和链接简介
第 2 部分 — 解析库中的符号

应用程序启动时,运行时链接程序负责加载应用程序所需的所有库。链接程序使用智能算法确定库的加载顺序,不过,有些编码样式可能会产生问题并导致非预期的行为。

本文为本系列文章的第一篇,介绍如何链接库以便应用程序能够在运行时找到这些库。

链接库

假定有一个应用程序依赖两个库。清单 1 显示了一个简单应用程序及其库的源代码。

清单 1:应用程序及其库
 $ more main.c
   #include <stdio.h>
 void f1();
   void f2();
 void main()
   {
   f1();
   f2();
   printf("In main\n");
   }
 $ more lib1.c
   #include <stdio.h>
 void f1()
   {
   printf("In library 1\n");
   }
 $ more lib2.c
   #include <stdio.h>
 void f2()
   {
   printf("In library 2\n");
   }

要构建库,编译时需要使用标志 -G 告知编译器输出为库,并使用标志 -Kpic 告知编译器生成位置无关代码。位置无关代码可以使库在内存中任何位置时都能工作,而这又可以使多个进程间共享同一库映像。此外,位置无关代码可减少重定位所需的工作量,从而提高链接过程的速度。清单 2 显示了生成这两个库的具体步骤。

清单 2:编译两个库
 $ cc -G -Kpic lib1.c -o lib1.so -z text
 $ cc -G -Kpic lib2.c -o lib2.so -z text

链接程序标志 -z text 使得链接程序在目标文件包含任何非可重定位代码的情况下报错,如清单 3 所示。

清单 3:尝试使用非可重定位目标文件构建库
 $ cc -G lib2.c -o lib2.so -z text
   Text relocation remains                         referenced
   against symbol                  offset      in file
   .rodata1 (section)                  0x14        lib2.o
   .rodata1 (section)                  0x18        lib2.o
   printf                              0x1c        lib2.o
   ld: fatal: relocations remain against allocatable but non-writable sections

构建主应用程序时,需要告知编译器链接这两个共享库。链接库的标志为 -l。因此,首次链接尝试可能会产生如清单 4 所示的结果。

清单 4:尝试在未指定库位置的情况下进行链接
 $ cc -o main main.c -l1 -l2
   ld: fatal: library -l1: not found
   ld: fatal: library -l2: not found
   ld: fatal: File processing errors. No output written to main

编译器不知道在何处找到库,因此无法链接库。应尝试在链接行显式列出这些库,如清单 5 所示。

清单 5:在链接命令中显式列出库
 $ cc -o main main.c lib1.so lib2.so
   $ ./main
   ld.so.1: main: fatal: lib1.so: open failed: No such file or directory

显式列出库看似有用,但应用程序在运行时失败。显而易见(但不好)的变通办法是使用 LD_LIBRARY_PATH 指定找到库的位置。清单 6 显示了这种不理想的办法。

清单 6:使用 LD_LIBRARY_PATH 掩盖不良的开发实践
 $ export LD_LIBRARY_PATH=`pwd`
   $ ./main
   In library 1
   In library 2
   In main

之所以说这是一个不好的选择,是因为现在应用程序需要依赖于环境设置才能正常工作。这意味着,在启动应用程序时需要使用脚本设置环境设置。这还使得程序有可能在与其他类似编码的应用程序一起工作时失败,甚至应用程序有可能从其他应用程序提取名称类似的库。环境标志 LD_LIBRARY_PATH 在开发过程中测试替代库实现时很有用,但它“不”应用作生产环境的一部分。

正确链接

正确的链接方法是使用 -L 选项(它指定链接时链接程序在何处可以找到库),以及 -R 选项(它指定在运行时应用程序可以在何处找到库)。清单 7 显示如何设置 -L-R 可以使应用程序正确构建和运行,而无需 LD_LIBRARY_PATH

清单 7:使用 -L 和 -R 设置编译时和运行时库路径
 $ cc -o main main.c -L. -R. -l1 -l2
   % ./main
   In library 1
   In library 2
   In main

清单 7 中的编译命令指定链接程序在编译时和运行时均搜索当前目录以查找所需库。在编译时,构建过程可以控制当前目录,因此可以正常工作。在运行时,不可能控制当前目录,结果此方法可能失败,如清单 8 所示。

清单 8:使用当前目录指定运行时库位置可能失败
 $ codes/library/main
   ld.so.1: main: fatal: lib1.so: open failed: No such file or directory
   Killed

另一种修复此问题的办法是使用绝对路径指示库的位置。这种办法对于系统库很有效,因为这些库在每个系统上都出现在完全相同的位置。但对于交付给用户的应用程序和库,情况就没那么乐观。用户可能对所选目录没有写入权限,系统上可能安装了应用程序的冲突版本,等等。

较好的解决方案是使用相对路径指定库的位置。这可以通过在链接时使用 $ORIGIN 标记来实现。此标记告知运行时链接程序:运行时路径是相对模块的位置来指定的。

清单 9 显示了此方法的一个示例。在此例中,使用 $ORIGIN 标记指示可在与可执行文件相同的目录下找到这两个库。可以使用相同的相对路径方法来指定某个库可以在何处找到它所依赖的库。

清单 9:使用 $ORIGIN 标记指示库的运行时位置
 $ cc -o main main.c -L. -R'$ORIGIN' -l1 -l2
   $ cd ../..
   $ codes/library/main
   In library 1
   In library 2
   In main

$ORIGIN 标记转义

使用 $ORIGIN 标记的一个复杂之处就在于它需要转义,使得 shell 不会处理它。确切的转义序列可能依赖于所使用的 shell 或 shell 版本。清单 9 显示了常用的使用单引号的序列。在 makefile 中,情况更复杂,需要两层转义才能向链接程序正确呈现标记,如清单 10 所示。

清单 10:在 Makefile 中使用 $ORIGIN 标记
RUNPATH = -R \$$ORIGIN

实际应用

在上述示例中,应用程序和库都位于当前目录中,现实情况并不完全如此。实际的应用程序通常安装在专用目录树中,或者分散在应用程序和库的标准位置。“在应用程序中使用和重新分发 Solaris Studio 库”一文中讨论了分发编译器自带库的最佳实践,但相同的原则也适用于应用程序所提供的库。

最佳实践总结

  • 使用 -L 指定编译时可以找到库的路径。
  • 使用 -R 指定运行时库的位置。
  • 使用标记 $ORIGIN 指定库位置的相对路径。这样就无需对库的位置进行硬编码。
       
修订版 1.0,2011 年 4 月 22 日